/**
 * File:   modbus_client_view_model.c
 * Author: AWTK Develop Team
 * Brief:  modbus_client_view_model
 *
 * Copyright (c) 2023 - 2024  Guangzhou ZHIYUAN Electronics Co.,Ltd.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * License file for more details.
 *
 */

/**
 * History:
 * ================================================================
 * 2024-2-07 Li XianJing <xianjimli@hotmail.com> created
 *
 */

#include "tkc/utils.h"
#include "tkc/object.h"
#include "conf_io/conf_node.h"
#include "tkc/func_call_parser.h"

#include "hmi_model.h"
#include "hmi_service.h"
#include "history_data.h"
#include "modbus_init_req.h"
#include "modbus_client_channel.h"
#include "modbus_client_view_model.h"
#include "modbus_client_channel_view_model.h"

#include "mvvm/mvvm.h"

#define MODBUS_CLIENT_MODEL(obj) modbus_client_model_cast(obj)

typedef struct _modbus_client_model_t {
  tk_object_t object;
  /**
   * @property {char*} url
   * @annotation ["readable"]
   * URL。
   */
  char* url;
  /**
   * @property {uint8_t} unit_id
   * @annotation ["readable"]
   * 单元ID。
   */
  uint8_t unit_id;
  /**
   * @property {modbus_client_t*} client
   * @annotation ["readable"]
   * 客户端。
   */
  modbus_client_t* client;
  /**
   * @property {bool_t} auto_reconnect
   * @annotation ["readable"]
   * 自动重连。
   */
  bool_t auto_reconnect;
  /**
   * @property {darray_t} channels
   * @annotation ["readable"]
   * 通道。
   */
  darray_t channels;
  /**
   * @property {tk_object_t*} variables
   * @annotation ["readable"]
   * 变量。
   */
  tk_object_t* variables;
  /**
   * @property {bool_t} running
   * @annotation ["readable"]
   * 后台线程运行中。
   */
  bool_t running;
  /**
   * @property {bool_t} quited
   * @annotation ["readable"]
   * 后台线程退出。
   */
  bool_t quited;
  /**
   * @property {tk_thread_t*} thread
   * @annotation ["readable"]
   * 后台线程。
   */
  tk_thread_t* thread;
  /**
   * @property {tk_mutex_t*} mutex
   * @annotation ["readable"]
   * 互斥锁。
   */
  tk_mutex_t* mutex;
  /**
   * @property {tk_semaphore_t*} sem
   * @annotation ["readable"]
   * 信号量。
   */
  tk_semaphore_t* sem;
  /**
   * @property {uint32_t} sleep_time
   * @annotation ["readable"]
   * 休眠时间。
   */
  uint32_t sleep_time;

  /**
   * @property {darray_t} history_data
   * @annotation ["readable"]
   * 历史数据。
   */
  darray_t history_data;

  /**
   * @property {uint8_t*} channels_init_by_read
   * @annotation ["readable"]
   * 通道初始化。
   */
  darray_t channels_init_by_read;

  /**
   * @property {darray_t} inits
   * @annotation ["readable"]
   * 初始化。
   */
  darray_t inits;

  /**
   * @property {bool_t} inited
   * @annotation ["readable"]
   * 初始化。
   */
  bool_t inited;
} modbus_client_model_t;

static ret_t idle_notify_updated(const idle_info_t* info);
static modbus_client_model_t* modbus_client_model_cast(tk_object_t* obj);

static modbus_client_channel_t* modbus_client_find_channel(modbus_client_model_t* agent,
                                                           const char* name) {
  uint32_t i = 0;
  return_value_if_fail(agent != NULL && name != NULL, NULL);

  for (i = 0; i < agent->channels.size; i++) {
    modbus_client_channel_t* channel = (modbus_client_channel_t*)darray_get(&(agent->channels), i);
    if (strcmp(channel->name, name) == 0) {
      return channel;
    }
  }

  return NULL;
}

static value_type_t modbus_client_model_get_prop_type(modbus_client_model_t* agent,
                                                      const char* type) {
  return tk_basic_type_from_name(type);
}

ret_t modbus_client_view_model_parse_prop(tk_object_t* obj, const char* name, modbus_prop_t* prop) {
  modbus_client_channel_t* channel = NULL;
  modbus_client_model_t* agent = MODBUS_CLIENT_MODEL(obj);
  return_value_if_fail(agent != NULL && name != NULL && prop != NULL, RET_BAD_PARAMS);

  return_value_if_fail(modbus_prop_parse(prop, agent->variables, name) == RET_OK, RET_BAD_PARAMS);

  channel = modbus_client_find_channel(agent, prop->channel_name);
  return_value_if_fail(channel != NULL, RET_NOT_FOUND);

  if (channel->read_buffer != NULL) {
    prop->buffer = channel->read_buffer;
    prop->buffer_length = channel->read_buffer_length;
  }

  if (channel->write_buffer != NULL) {
    prop->buffer = channel->write_buffer;
    prop->buffer_length = channel->write_buffer_length;
  }
  return_value_if_fail(prop->buffer != NULL, RET_BAD_PARAMS);

  return RET_OK;
}

static ret_t modbus_client_load_history_data(modbus_client_model_t* agent, conf_node_t* node) {
  const char* field = NULL;
  conf_node_t* iter = conf_node_get_first_child(node);
  return_value_if_fail(agent != NULL && node != NULL, RET_BAD_PARAMS);

  while (iter != NULL) {
    value_t v;
    if (conf_node_get_value(iter, &v) == RET_OK) {
      field = value_str(&v);
      if (field != NULL) {
        darray_push(&(agent->history_data), tk_strdup(field));
      }
    }
    iter = iter->next;
  }

  return RET_OK;
}

static ret_t modbus_client_load_channels(modbus_client_model_t* agent, conf_node_t* node) {
  conf_node_t* iter = conf_node_get_first_child(node);
  return_value_if_fail(agent != NULL && node != NULL, RET_BAD_PARAMS);

  agent->sleep_time = 500;
  while (iter != NULL) {
    bool_t init_by_read = conf_node_get_child_value_bool(iter, "init_by_read", FALSE);
    modbus_client_channel_t* channel = modbus_client_channel_create(iter);
    return_value_if_fail(channel != NULL, RET_OOM);

    if (agent->sleep_time > channel->update_interval) {
      agent->sleep_time = channel->update_interval;
    }

    darray_push(&(agent->channels), channel);
    if (init_by_read) {
      darray_push(&(agent->channels_init_by_read), channel);
    }

    iter = iter->next;
  }

  return RET_OK;
}

static ret_t modbus_client_load_inits(modbus_client_model_t* agent, conf_node_t* node) {
  conf_node_t* iter = conf_node_get_first_child(node);
  return_value_if_fail(agent != NULL && node != NULL, RET_BAD_PARAMS);

  while (iter != NULL) {
    modbus_init_req_t* init = modbus_init_req_create(iter);
    return_value_if_fail(init != NULL, RET_OOM);

    darray_push(&(agent->inits), init);
    iter = iter->next;
  }

  return RET_OK;
}

static ret_t modbus_client_load_variables(modbus_client_model_t* agent, conf_node_t* node) {
  value_t v;
  conf_node_t* iter = conf_node_get_first_child(node);

  while (iter != NULL) {
    const char* name = conf_node_get_name(iter);

    if (conf_node_get_value(iter, &v) == RET_OK) {
      modbus_prop_t* prop = modbus_prop_create(NULL, value_str(&v));
      log_debug("%s = %s\n", name, value_str(&v));
      if (prop != NULL) {
        tk_object_set_prop_pointer_ex(agent->variables, name, prop,
                                      (tk_destroy_t)modbus_prop_destroy);
      } else {
        log_debug("modbus_prop_create failed:%s\n", name);
      }
    }

    iter = iter->next;
  }

  return RET_OK;
}

static ret_t modbus_client_send_init_requests(modbus_client_model_t* agent) {
  uint32_t i = 0;
  darray_t* inits = NULL;
  return_value_if_fail(agent != NULL, RET_BAD_PARAMS);

  inits = &(agent->inits);
  for (i = 0; i < inits->size; i++) {
    modbus_init_req_t* init = (modbus_init_req_t*)darray_get(inits, i);
    modbus_init_req_request(init, agent->client);
  }

  return RET_OK;
}

static ret_t modbus_client_init_by_read(modbus_client_model_t* agent) {
  uint32_t i = 0;
  darray_t* channels = NULL;
  return_value_if_fail(agent != NULL, RET_BAD_PARAMS);

  channels = &(agent->channels_init_by_read);
  return_value_if_fail(agent != NULL, RET_BAD_PARAMS);
  for (i = 0; i < channels->size; i++) {
    modbus_client_channel_t c;
    modbus_client_channel_t* iter = (modbus_client_channel_t*)darray_get(channels, i);

    memset(&c, 0, sizeof(c));
    c.client = agent->client;
    c.unit_id = iter->unit_id;
    c.bits_buffer = iter->bits_buffer;
    c.bits_length = iter->bits_length;
    c.read_buffer = iter->write_buffer;
    c.read_offset = iter->write_offset;
    c.read_buffer_length = iter->write_buffer_length;

    switch (iter->access_type) {
      case MODBUS_FC_WRITE_MULTIPLE_COILS: {
        c.access_type = MODBUS_FC_READ_COILS;
        break;
      }
      case MODBUS_FC_WRITE_MULTIPLE_HOLDING_REGISTERS: {
        c.access_type = MODBUS_FC_READ_HOLDING_REGISTERS;
        break;
      }
      case MODBUS_FC_WRITE_SINGLE_COIL: {
        c.access_type = MODBUS_FC_READ_COILS;
        break;
      }
      case MODBUS_FC_WRITE_SINGLE_HOLDING_REGISTER: {
        c.access_type = MODBUS_FC_READ_HOLDING_REGISTERS;
        break;
      }
      default: {
        log_debug("invalid access_type:%d\n", iter->access_type);
        break;
      }
    }

    if (c.access_type) {
      ret_t ret = modbus_client_channel_read(&c);
      log_debug("init_by_read: %s ret=%d offset=%d length=%d\n", iter->name, ret, c.read_offset,
                c.read_buffer_length);
    }
  }

  return RET_OK;
}

static ret_t modbus_client_connect(modbus_client_model_t* agent) {
  uint32_t i = 0;
  return_value_if_fail(agent != NULL, RET_BAD_PARAMS);

  if (agent->client != NULL) {
    return RET_OK;
  }

  agent->client = modbus_client_create(agent->url);
  if (!agent->auto_reconnect) {
    return_value_if_fail(agent->client != NULL, RET_FAIL);
  }

  for (i = 0; i < agent->channels.size; i++) {
    modbus_client_channel_t* channel = (modbus_client_channel_t*)darray_get(&(agent->channels), i);
    modbus_client_channel_set_client(channel, agent->client);
  }

  if (!agent->inited) {
    modbus_client_send_init_requests(agent);
    agent->inited = TRUE;
  }

  modbus_client_init_by_read(agent);
  idle_queue(idle_notify_updated, agent);

  return RET_OK;
}

static ret_t modbus_client_load_conf(modbus_client_model_t* agent, conf_doc_t* doc) {
  conf_node_t* node = NULL;
  return_value_if_fail(agent != NULL && doc != NULL, RET_BAD_PARAMS);

  agent->url = tk_strdup(conf_doc_get_str(doc, "url", NULL));
  agent->unit_id = conf_doc_get_int(doc, "unit_id", 0xff);
  agent->auto_reconnect = conf_doc_get_bool(doc, "auto_reconnect", TRUE);
  return_value_if_fail(agent->url != NULL, RET_BAD_PARAMS);

  node = conf_node_find_child(doc->root, "channels");
  if (node != NULL) {
    modbus_client_load_channels(agent, node);
  }

  node = conf_node_find_child(doc->root, "inits");
  if (node != NULL) {
    modbus_client_load_inits(agent, node);
  }

  node = conf_node_find_child(doc->root, HISTORY_DATA_PROP_NAME);
  if (node != NULL) {
    modbus_client_load_history_data(agent, node);
  }

  node = conf_node_find_child(doc->root, "variables");
  if (node != NULL) {
    modbus_client_load_variables(agent, node);
  }

  return RET_OK;
}

ret_t modbus_client_model_set_prop_impl(tk_object_t* obj, modbus_prop_t* prop, const value_t* v) {
  ret_t ret = RET_OK;
  modbus_client_model_t* agent = MODBUS_CLIENT_MODEL(obj);
  return_value_if_fail(agent != NULL && prop != NULL && v != NULL, RET_BAD_PARAMS);

  tk_mutex_lock(agent->mutex);
  ret = modbus_prop_set_value(prop, v);
  tk_mutex_unlock(agent->mutex);

  return ret;
}

static ret_t modbus_client_model_set_prop(tk_object_t* obj, const char* name, const value_t* v) {
  modbus_prop_t prop;
  return_value_if_fail(obj != NULL && name != NULL && v != NULL, RET_BAD_PARAMS);

  if (tk_str_ieq(name, MODBUS_CLIENT_MODEL_PROP_CONNECTED)) {
    return RET_NOT_FOUND;
  } else if (tk_str_ieq(name, MODBUS_CLIENT_MODEL_PROP_URL)) {
    return RET_NOT_FOUND;
  }

  if (modbus_client_view_model_parse_prop(obj, name, &prop) == RET_OK) {
    return modbus_client_model_set_prop_impl(obj, &prop, v);
  } else {
    log_debug("modbus_client_view_model_parse_prop failed:%s\n", name);
  }

  return RET_NOT_FOUND;
}

ret_t modbus_client_model_get_prop_impl(tk_object_t* obj, modbus_prop_t* prop, value_t* v) {
  modbus_client_model_t* agent = MODBUS_CLIENT_MODEL(obj);
  return_value_if_fail(obj != NULL && prop != NULL && v != NULL, RET_BAD_PARAMS);

  tk_mutex_lock(agent->mutex);
  ret_t ret = modbus_prop_get_value(prop, v);
  tk_mutex_unlock(agent->mutex);

  return ret;
}

static ret_t modbus_client_model_get_prop(tk_object_t* obj, const char* name, value_t* v) {
  modbus_prop_t prop;
  modbus_client_model_t* agent = MODBUS_CLIENT_MODEL(obj);
  return_value_if_fail(obj != NULL && name != NULL && v != NULL, RET_BAD_PARAMS);

  if (tk_str_ieq(name, MODBUS_CLIENT_MODEL_PROP_CONNECTED)) {
    value_set_bool(v, agent->client != NULL);
    return RET_OK;
  } else if (tk_str_ieq(name, MODBUS_CLIENT_MODEL_PROP_URL)) {
    value_set_str(v, agent->url);
    return RET_OK;
  }

  if (modbus_client_view_model_parse_prop(obj, name, &prop) == RET_OK) {
    return modbus_client_model_get_prop_impl(obj, &prop, v);
  } else {
    log_debug("modbus_client_view_model_parse_prop failed:%s\n", name);
  }

  return RET_NOT_FOUND;
}

static ret_t modbus_client_model_on_destroy(tk_object_t* obj) {
  modbus_client_model_t* agent = MODBUS_CLIENT_MODEL(obj);
  return_value_if_fail(agent != NULL, RET_BAD_PARAMS);

  agent->running = FALSE;
  while (!agent->quited) {
    sleep_ms(10);
  }
  tk_thread_destroy(agent->thread);
  tk_mutex_destroy(agent->mutex);
  tk_semaphore_destroy(agent->sem);
  darray_deinit(&agent->channels);
  darray_deinit(&agent->channels_init_by_read);
  darray_deinit(&agent->history_data);
  darray_deinit(&agent->inits);

  TKMEM_FREE(agent->url);
  TK_OBJECT_UNREF(agent->variables);
  if (agent->client != NULL) {
    modbus_client_destroy(agent->client);
  }

  return RET_OK;
}

static bool_t modbus_client_model_can_exec(tk_object_t* obj, const char* name, const char* args) {
  modbus_client_model_t* agent = MODBUS_CLIENT_MODEL(obj);
  return_value_if_fail(obj != NULL && name != NULL, FALSE);

  if (tk_str_ieq(name, MODBUS_CLIENT_MODEL_CMD_UPDATE)) {
    return agent->client != NULL;
  }

  return FALSE;
}

static ret_t modbus_slave_request_update(modbus_client_model_t* agent, const char* name) {
  uint32_t i = 0;
  return_value_if_fail(agent != NULL, RET_BAD_PARAMS);

  for (i = 0; i < agent->channels.size; i++) {
    modbus_client_channel_t* channel = (modbus_client_channel_t*)darray_get(&(agent->channels), i);
    if (name == NULL || *name == '\0' || tk_str_eq(name, channel->name)) {
      channel->next_update_time = 0;
    }
  }

  tk_semaphore_post(agent->sem);

  return RET_OK;
}

static ret_t modbus_client_model_exec(tk_object_t* obj, const char* name, const char* args) {
  modbus_client_model_t* agent = MODBUS_CLIENT_MODEL(obj);
  return_value_if_fail(obj != NULL && name != NULL, RET_BAD_PARAMS);

  if (tk_str_ieq(name, MODBUS_CLIENT_MODEL_CMD_UPDATE)) {
    modbus_slave_request_update(agent, args);
  }

  return RET_NOT_FOUND;
}

static const object_vtable_t s_modbus_client_vtable = {
    .type = "modbus_client",
    .desc = "modbus_client",
    .size = sizeof(modbus_client_model_t),
    .is_collection = FALSE,
    .on_destroy = modbus_client_model_on_destroy,
    .get_prop = modbus_client_model_get_prop,
    .set_prop = modbus_client_model_set_prop,
    .can_exec = modbus_client_model_can_exec,
    .exec = modbus_client_model_exec,
};

static ret_t prepare_and_set_history_data(modbus_client_model_t* agent) {
  str_t str;
  value_t v;
  uint32_t i = 0;
  char buff[64] = {0};
  date_time_t dt;
  tk_object_t* obj = hmi_service_get_default_model();
  return_value_if_fail(agent != NULL && obj != NULL, RET_BAD_PARAMS);

  str_init(&str, 256);
  date_time_init(&dt);
  str_append_int64(&str, date_time_to_time(&dt));
  for (i = 0; i < agent->history_data.size; i++) {
    const char* field = (const char*)darray_get(&(agent->history_data), i);
    str_append(&str, "|");
    if (fscript_eval(TK_OBJECT(agent), field, &v) == RET_OK) {
      str_append(&str, value_str_ex(&v, buff, sizeof(buff)));
    } else {
      str_append(&str, "0");
      log_debug("fscript_eval failed:%s\n", field);
    }
  }

  tk_object_set_prop_str(obj, HISTORY_DATA_PROP_NAME, str.str);
  str_reset(&str);

  return RET_OK;
}

static ret_t idle_notify_updated(const idle_info_t* info) {
  modbus_client_model_t* agent = (modbus_client_model_t*)info->ctx;

  if (agent->history_data.size > 0) {
    prepare_and_set_history_data(agent);
  }

  emitter_dispatch_simple_event(EMITTER(agent), EVT_PROP_CHANGED);
  emitter_dispatch_simple_event(EMITTER(agent), EVT_PROPS_CHANGED);
  log_debug("idle_notify_updated\n");

  return RET_REMOVE;
}

static ret_t modbus_client_update(modbus_client_model_t* agent) {
  uint32_t i = 0;
  bool_t changed = FALSE;
  return_value_if_fail(agent != NULL, RET_BAD_PARAMS);

  for (i = 0; i < agent->channels.size; i++) {
    uint64_t now = time_now_ms();
    modbus_client_channel_t* channel = (modbus_client_channel_t*)darray_get(&(agent->channels), i);

    if (strstr(agent->url, "tcp") == NULL) {
      uint8_t unit_id = channel->unit_id ? channel->unit_id : agent->unit_id;
      modbus_client_set_slave(agent->client, unit_id);
    }

    ret_t ret = modbus_client_channel_update(channel, now);
    if (ret == RET_IO) {
      log_debug("modbus_client_channel_update failed\n");
      modbus_client_destroy(agent->client);
      agent->client = NULL;
      changed = TRUE;
    } else if (ret == RET_OK) {
      changed = TRUE;
    }
  }

  if (changed) {
    log_debug("modbus_client_channel_update ok\n");
    idle_queue(idle_notify_updated, agent);
  }

  return RET_OK;
}

static void* thread_func(void* args) {
  modbus_client_model_t* agent = (modbus_client_model_t*)args;
  return_value_if_fail(agent != NULL, NULL);

  agent->running = TRUE;
  agent->quited = FALSE;
  modbus_client_connect(agent);

  log_debug("modbus client thread_func enter\n");
  while (agent->running) {
    if (agent->client == NULL) {
      if (agent->auto_reconnect) {
        modbus_client_connect(agent);
        if (agent->client == NULL) {
          sleep_ms(500);
          continue;
        }
      } else {
        break;
      }
    }

    tk_semaphore_wait(agent->sem, agent->sleep_time);
    if (agent->running) {
      modbus_client_update(agent);
    }
  }
  log_debug("modbus client thread_func exit\n");

  agent->quited = TRUE;

  return NULL;
}

static tk_object_t* modbus_client_model_create(conf_doc_t* doc) {
  tk_object_t* obj = NULL;
  modbus_client_model_t* agent = NULL;
  return_value_if_fail(doc != NULL, NULL);

  obj = object_create(&s_modbus_client_vtable);
  return_value_if_fail(obj != NULL, NULL);
  agent = (modbus_client_model_t*)obj;

  agent->variables = object_default_create();
  darray_init(&agent->channels, 10, (tk_destroy_t)modbus_client_channel_destroy, NULL);
  darray_init(&agent->channels_init_by_read, 10, NULL, NULL);
  darray_init(&agent->history_data, 10, default_destroy, NULL);
  darray_init(&agent->inits, 10, (tk_destroy_t)modbus_init_req_destroy, NULL);

  if (modbus_client_load_conf(agent, doc) == RET_OK) {
    agent->thread = tk_thread_create(thread_func, agent);
    agent->mutex = tk_mutex_create();
    agent->sem = tk_semaphore_create(0, "modbus");
    tk_thread_start(agent->thread);
    return obj;
  } else {
    TK_OBJECT_UNREF(obj);
  }

  return NULL;
}

static modbus_client_model_t* modbus_client_model_cast(tk_object_t* obj) {
  return_value_if_fail(obj != NULL, NULL);
  return_value_if_fail(obj->vt == &s_modbus_client_vtable, NULL);

  return (modbus_client_model_t*)obj;
}

tk_object_t* modbus_client_view_model_create_with_name(const char* name) {
  conf_doc_t* doc = NULL;
  tk_object_t* obj = NULL;
  char config_name[MAX_PATH + 1] = {0};
  tk_snprintf(config_name, MAX_PATH, "%s.json", name);
  doc = hmi_model_load_conf_from_asset(config_name);

  if (doc != NULL) {
    obj = modbus_client_model_create(doc);
    conf_doc_destroy(doc);
    tk_object_set_name(obj, name);
    return obj;
  }

  return NULL;
}

static tk_object_t* s_share_vms;

view_model_t* modbus_client_view_model_create(navigator_request_t* req) {
  const char* type_and_args = NULL;
  return_value_if_fail(req != NULL, NULL);

  type_and_args = tk_object_get_prop_str(req->args, NAVIGATOR_ARG_VIEW_MODEL_TYPE);
  if (type_and_args != NULL && strchr(type_and_args, '(') != NULL) {
    tk_object_t* obj = NULL;
    view_model_t* vm = NULL;
    tk_object_t* args = func_call_parse(type_and_args, tk_strlen(type_and_args));
    const char* config = tk_object_get_prop_str(args, "name");
    bool_t share = tk_object_get_prop_bool(args, "share", FALSE);

    if (share) {
      obj = tk_object_get_prop_object(s_share_vms, config);
      if (obj == NULL) {
        obj = modbus_client_view_model_create_with_name(config);
        if (obj != NULL) {
          tk_object_set_prop_object(s_share_vms, config, obj);
        }
      } else {
        tk_object_ref(obj);
      }
    } else {
      obj = modbus_client_view_model_create_with_name(config);
    }
    TK_OBJECT_UNREF(args);
    return_value_if_fail(obj != NULL, NULL);

    vm = view_model_object_wrapper_create(obj);
    tk_object_set_name(TK_OBJECT(vm), obj->name);
    TK_OBJECT_UNREF(obj);

    return vm;
  }

  return NULL;
}

ret_t modbus_client_view_model_deinit(void) {
  TK_OBJECT_UNREF(s_share_vms);

  return RET_OK;
}

ret_t modbus_client_view_model_init(void) {
  s_share_vms = object_default_create_ex(FALSE);

  view_model_factory_register("modbus_client_channel", modbus_client_channel_view_model_create);
  return view_model_factory_register("modbus_client", modbus_client_view_model_create);
}
